#pragma once

#include <iostream>
#include <stdexcept>
#include <dlfcn.h>


namespace dlloader
{
    class DLLoader
    {
    private:

        void              *_handle;
        std::string        _pathToLib;
        std::string        _allocClassSymbol;
        std::string        _deleteClassSymbol;

    public:

        DLLoader(
            std::string const &allocClassSymbol = "allocator",
            std::string const &deleteClassSymbol = "deleter") :
            _handle(nullptr),
            _allocClassSymbol(allocClassSymbol), _deleteClassSymbol(deleteClassSymbol)
        {
        }

        ~DLLoader() = default;

        int openLib(const std::string &pathToLib)
        {
            _pathToLib = pathToLib;
            if (!(_handle = dlopen(_pathToLib.c_str(), RTLD_NOW | RTLD_LAZY))) {
                std::cerr << "Can't open and load " << _pathToLib << ", error code: " << last_error() << std::endl;
                return -1;
            }

            return 0;
        }

        template <class T>
        std::shared_ptr<T> getClassInstance()
        {
            using allocClass = T *(*)();
            using deleteClass = void (*)(T *);


            auto allocFunc = reinterpret_cast<allocClass>(
                    dlsym(_handle, _allocClassSymbol.c_str()));
            auto deleteFunc = reinterpret_cast<deleteClass>(
                    dlsym(_handle, _deleteClassSymbol.c_str()));

            if (!allocFunc || !deleteFunc) {
                //closeLib();
                std::cerr << "Can't find allocator or deleter symbol in " << _pathToLib << ", error code: " << last_error() << std::endl;
            }

            return std::shared_ptr<T>(
                    allocFunc(),
                    [deleteFunc](T *p){ deleteFunc(p); });
        }

        template <class T>
        std::shared_ptr<T>    getFuncInstance(const std::string& func_name)
        {
            void* func_ptr = ::dlsym(_handle, func_name.c_str());
            if (func_ptr == nullptr)
            {
                std::cerr << "Can't find " << func_name << ", error code: " << last_error() << std::endl;
            }

            T* func = reinterpret_cast<T*>(func_ptr);
            return std::shared_ptr<T>(func, [](T* ptr){});
        }

        void closeLib()
        {
            if (dlclose(_handle) != 0) {
                std::cerr << "Can't close " << _pathToLib << ", error code: " << last_error() << std::endl;
            }
        }

        std::string last_error()
        {
            return std::string(::dlerror());
        }
    };
}
